//go:build cgo

package navcat

import (
	"bufio"
	"bytes"
	_ "context"
	"database/sql"
	"encoding/binary"
	"errors"
	"gitee.com/yysf-xin/dproxy/pkg/app"
	"github.com/go-chi/chi/v5"
	"github.com/mattn/go-sqlite3"
	"golang.org/x/sync/singleflight"
	"io"
	"net/http"
	"net/http/httputil"
	"os"
	"sync"
	"time"
	_ "unsafe"
)

const (
	SQLITE = uint8(iota)
	MYSQL
)

type Navicat struct {
	cache sync.Map
	one   singleflight.Group
}
type DbHolder struct {
	db       *sql.DB
	lastTime int64
	t        uint8
}

func (n *Navicat) DoClean(w http.ResponseWriter, req *http.Request) {
	key := req.URL.Query().Get("key")
	if v, ok := n.cache.Load(key); ok {
		_ = v.(*DbHolder).db.Close()
		n.cache.Delete(key)
	}
	app.RenderJSON(w, nil, nil)
}
func (n *Navicat) ServeHTTP(w http.ResponseWriter, req *http.Request) {
	var buff = bufio.NewWriter(w)
	defer func() {
		_ = buff.Flush()
	}()

	f := req.PostFormValue("dbfile")
	var holder *DbHolder
	if f != "" {
		key := "sqlite3:" + f
		dbV, ok := n.cache.Load(key)
		if !ok {
			v, err, _ := n.one.Do(key, func() (holder interface{}, err error) {
				_, err = os.Stat(f)
				if err != nil {
					return
				}
				db, err := sql.Open("sqlite3", f)
				if err == nil && db != nil {
					holder = &DbHolder{
						db:       db,
						t:        SQLITE,
						lastTime: time.Now().UnixMilli(),
					}
					n.cache.Store(key, holder)
				}
				return
			})

			if err != nil {
				writeError(buff, err)
				return
			}
			dbV = v
		}
		holder = dbV.(*DbHolder)
	}

	if holder == nil {
		data, err := httputil.DumpRequest(req, true)
		if err == nil {
			println(string(data))
		}
		writeError(buff, errors.New("err params"))
		return
	}

	action := req.PostFormValue("actn")

	writeHeader(buff, 0)

	if action == "C" {
		if holder.t == SQLITE {
			v1, _, _ := sqlite3.Version()
			version := []byte(v1)
			writeBlock(buff, version)
			writeBlock(buff, version)
			writeBlock(buff, version)
			return
		}
	}

	if action == "Q" {
		queries := req.PostForm["q[]"]
		l := len(queries)
		for i, v := range queries {
			if v == "" {
				continue
			}
			st, err := holder.db.Prepare(v)
			var rs resultSet
			var cols []string

			var rowBuf bytes.Buffer

			if err == nil {
				row, err := st.QueryContext(req.Context())
				if err == nil {
					cols, _ = row.Columns()
					rs.Numfields = uint32(len(cols))
					//colTypes,_ := row.ColumnTypes()
					values := make([]interface{}, len(cols))

					for i := 0; i < len(cols); i++ {
						values[i] = &sql.RawBytes{}
					}

					for row.Next() {
						err = row.Scan(values...)
						if err == nil {
							rs.Numrows++
							for _, v := range values {
								if v == nil {
									rowBuf.WriteByte(0xff)
								} else {
									writeBlock(&rowBuf, *(v.(*sql.RawBytes)))
								}
								_ = binary.Write(&rowBuf, binary.BigEndian, int32(0))
							}
						}
					}
					_ = row.Close()
				}
			}

			if err == nil {
				writeResultHeader(buff, &rs)
				if rs.Numfields > 0 {
					for _, v := range cols {
						writeBlock(buff, []byte(v))
						writeBlock(buff, []byte(""))
						_ = binary.Write(buff, binary.BigEndian, int32(5))
						_ = binary.Write(buff, binary.BigEndian, uint32(0))
						_ = binary.Write(buff, binary.BigEndian, uint32(0))
					}

					if rs.Numrows > 0 {
						_, _ = buff.Write(rowBuf.Bytes())
					}
				} else {
					writeBlock(buff, []byte(""))
				}
			} else {
				if sqlErr, ok := err.(sqlite3.Error); ok {
					rs.Errno = uint32(sqlErr.Code)
				} else {
					rs.Errno = 1
				}
				writeResultHeader(buff, &rs)
				writeBlock(buff, []byte(err.Error()))
			}
			if i < l-1 {
				_ = buff.WriteByte(0x01)
			} else {
				_ = buff.WriteByte(0)
			}

			if st != nil {
				_ = st.Close()
			}
		}
	}
}
func GetDummy(buff io.ByteWriter, l int) {
	for i := 0; i < l; i++ {
		_ = buff.WriteByte(0)
	}
}

type resultSet struct {
	Errno, Affectrows, Insertid, Numfields, Numrows uint32
}

func writeResultHeader(buff bufWriter, rs *resultSet) {
	_ = binary.Write(buff, binary.BigEndian, rs)
	GetDummy(buff, 12)

}

func writeError(buff bufWriter, err error) {
	writeHeader(buff, 202)
	writeBlock(buff, []byte(err.Error()))
}
func writeHeader(buff bufWriter, code uint32) {
	_ = binary.Write(buff, binary.BigEndian, uint32(1111))
	_ = binary.Write(buff, binary.BigEndian, uint16(202))
	_ = binary.Write(buff, binary.BigEndian, code)
	GetDummy(buff, 6)
}

type bufWriter interface {
	io.Writer
	io.ByteWriter
}

func writeBlock(buff bufWriter, data []byte) {
	l := len(data)
	if l < 254 {
		_ = binary.Write(buff, binary.BigEndian, uint8(l))
	} else {
		_ = buff.WriteByte(0xfe)
		_ = binary.Write(buff, binary.BigEndian, uint32(l))
	}
	_, _ = buff.Write(data)
}

func Init(api chi.Router) {
	api.Route("/navicat", func(r chi.Router) {
		navicat := new(Navicat)
		r.HandleFunc("/clean", navicat.DoClean)
		r.HandleFunc("/sqlite", navicat.ServeHTTP)
	})
}
